package se.webbzon.oschi01.tools.converter;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import se.webbzon.oschi01.nbt.ByteArrayTag;
import se.webbzon.oschi01.nbt.ByteArrayTagFactory;
import se.webbzon.oschi01.nbt.CompoundTag;
import se.webbzon.oschi01.nbt.NBTCompression;
import se.webbzon.oschi01.nbt.NBTException;
import se.webbzon.oschi01.nbt.NBTInputStreamReader;
import se.webbzon.oschi01.nbt.NBTOutputStreamWriter;
import se.webbzon.oschi01.nbt.ShortTag;
import se.webbzon.oschi01.nbt.Tag;
import se.webbzon.oschi01.nbt.Tags;
import se.webbzon.oschi01.singleton.Singletons;
import se.webbzon.oschi01.utils.Progress;
import se.webbzon.oschi01.world.Chunk;
import se.webbzon.oschi01.world.Dimension;
import se.webbzon.oschi01.world.Region;
import se.webbzon.oschi01.world.World;

public class Converter {
	
	private final Map<Integer,Integer> idMap;
	
	private int fallbackBlockId;
	
	private int fallbackItemId;
	
	private boolean resolveItemStructures;
	
	public Converter() {
		this(new HashMap<Integer,Integer>(),true);
	}
	
	public Converter(Map<Integer,Integer> idMap, boolean resolveItemStructures) {
		this.idMap = idMap;
		this.fallbackBlockId = 0;
		this.fallbackItemId = 1;
		this.resolveItemStructures = resolveItemStructures;
	}
	
	public int getMapping(int itemId) {
		Integer i = idMap.get(itemId);
		if (i == null)
			return -1;
		else
			return i;
	}
	
	public synchronized void addMapping(int oldItemId, int newItemId) {
		idMap.put(oldItemId, newItemId);
	}
	
	public synchronized boolean removeMapping(int itemId) {
		return idMap.remove(itemId) != null;
	}
	
	public synchronized void clearMapping() {
		idMap.clear();
	}

	public int getFallbackBlockId() {
		return fallbackBlockId;
	}

	public synchronized boolean setFallbackBlockId(int fallbackBlockId) {
		if (fallbackBlockId >= 0 && fallbackBlockId < 65536) {
			this.fallbackBlockId = fallbackBlockId;
			return true;
		} else
			return false;
	}

	public int getFallbackItemId() {
		return fallbackItemId;
	}

	public synchronized boolean setFallbackItemId(int fallbackItemId) {
		if (fallbackItemId >= 0 && fallbackItemId < 65536) {
			this.fallbackItemId = fallbackItemId;
			return true;
		} else
			return false;
	}

	public boolean isResolveItemStructures() {
		return resolveItemStructures;
	}

	public synchronized void setResolveItemStructures(boolean resolveItemStructures) {
		this.resolveItemStructures = resolveItemStructures;
	}

	public Progress<Object> convert(final World world) throws IOException {
		final Progress<Object> progress = new Progress<Object>();
		
		Thread thread = new Thread() {
		
			@Override public void run() {
				// Count the number of chunks
				int chunks = 0;
				for (int i = 0; i < world.dimensions(); i++) {
					Dimension d = world.getDimension(i);
					for (int j = 0; j < d.regions(); j++) {
						chunks += d.getRegion(j).chunks();
					}
				}
				
				// Convert chunks
				synchronized (Converter.this) {
					int chunk = 0;
					for (int i = 0; i < world.dimensions(); i++) {
						Dimension d = world.getDimension(i);
						for (int j = 0; j < d.regions(); j++) {
							Region r = d.getRegion(j);
							for (int k = 0; k < r.chunks(); k++) {
								Chunk c = r.getChunk(k);
								progress.setProgress(90 * chunk / chunks, "Converting chunk " + c + " in dimension " + d.getName());
								try {
									Tag<?> root = c.getRoot();
									nbtConvert(root);
									chunk++;
								} catch (NBTException e) {
									progress.abort(e);
									return;
								}
							}
							try {
								r.writeback();
							} catch (IOException e) {
								e.printStackTrace();
								progress.abort(e);
							}
						}
					}
					
					// Convert data
					Collection<String> data = world.getData();
					int length = data.size();
					int i = 0;
					for (String datum : data) {
						progress.setProgress(90 + 10 * i / length, "Converting " + datum);
						for(NBTCompression cmp : NBTCompression.values()) try {
							Tag<?> root = world.getDatum(datum, new NBTInputStreamReader(cmp));
							nbtConvert(root);
							world.setDatum(datum, root, new NBTOutputStreamWriter(cmp));
						} catch (IOException e) {
							// Do nothing
						}
						i++;
					}
				}
				progress.complete(null, "Conversion complete!");
			}
			
			@Override public void interrupt() {
				super.interrupt();
				progress.abort(new InterruptedException());
			}
			
		};
		
		thread.start();
		return progress;
	}
	
	private final <T> void nbtConvert(Tag<T> tag) {
		if (isSections(tag))
			convertSections(tag);
		else if (isItemStructure(tag))
			convertItemStructure(tag);
		else if (tag.hasChildren()) {
			for (Tag<?> child : tag.getChildren())
				nbtConvert(child);
		}
	}
	
	private final <T> boolean isSections(Tag<T> tag) {
		if (Tags.isListTag(tag) && tag.hasParent()) {
			Tag<?> parent = tag.getParent();
			if (tag.toString().equalsIgnoreCase("sections") && parent.hasParent()) {
				Tag<?> grandParent = parent.getParent();
				if (parent.toString().equalsIgnoreCase("level"))
					return !grandParent.hasParent();
				else
					return false;
			} else
				return false;
		} else
			return false;
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private final <T> void convertSections(Tag<T> sections) {
		if (sections.hasChildren()) {
			for (Tag<?> child : sections.getChildren()) {
				if (Tags.isCompoundTag(child) && child.hasChildren()) {
					CompoundTag section = (CompoundTag) child;
					ByteArrayTag blocksTag = null, addTag = null;
					for (Tag<?> data : section.getChildren()) {
						if (Tags.isByteArrayTag(data)) {
							String name = data.toString().toLowerCase();
							if (name.equals("blocks"))
								blocksTag = (ByteArrayTag) data;
							else if (name.equals("add"))
								addTag = (ByteArrayTag) data;
						} 
					}
					if (blocksTag != null) {
						byte[] oldBlocks = blocksTag.getValue();
						byte[] oldAdd;
						if (addTag == null)
							oldAdd = new byte[2048];
						else
							oldAdd = addTag.getValue();
						byte[] newBlocks = new byte[oldBlocks.length];
						byte[] newAdd = new byte[oldAdd.length];
						for (int i = 0; i < oldAdd.length; i++) {
							int o1 = (0x00000f00 & (oldAdd[i] << 8)) | (0x000000ff & oldBlocks[2*i]);
							int o2 = (0x00000f00 & (oldAdd[i] << 4)) | (0x000000ff & oldBlocks[2*i+1]);
							int n1 = getMapping(o1);
							int n2 = getMapping(o2);
							n1 = n1 == -1 ? getFallbackBlockId() : n1;
							n2 = n2 == -1 ? getFallbackBlockId() : n2;
							newBlocks[2*i] = (byte) n1;
							newBlocks[2*i+1] = (byte) n2;
							newAdd[i] = (byte) ((0x0000000f & (n1 >> 8)) | (0x000000f0 & (n2 >> 4)));
						}
						blocksTag.setValue(newBlocks);
						if (addTag == null) {
							addTag = new ByteArrayTag(Singletons.instance(ByteArrayTagFactory.class),"Add",newAdd);
							section.getChildren().addSibling((Tag) addTag);
						} else
							addTag.setValue(newAdd);
					}
				}
			}
		}
	}
	
	private final <T> boolean isItemStructure(Tag<T> tag) {
		boolean idFound = false;
		int othersFound = 0;
		if (tag.hasChildren()) {
			for (Tag<?> child : tag.getChildren()) {
				String name = child.toString().toLowerCase();
				if (name.equals("id") && Tags.isShortTag(child))
					idFound = true;
				else if (name.equals("count"))
					othersFound++;
				else if (name.equals("slot"))
					othersFound++;
				else if (name.equals("damage"))
					othersFound++;
				else if (name.equals("tag"))
					othersFound++;
			}
		}
		return idFound && othersFound > 0;
	}
	
	private final <T> void convertItemStructure(Tag<T> itemStructure) {
		for (Tag<?> child : itemStructure.getChildren()) {
			String name = child.toString().toLowerCase();
			if (Tags.isShortTag(child) && name.equals("id")) {
				ShortTag id = (ShortTag) child;
				int oldId = id.getValue();
				int newId = getMapping(oldId);
				newId = newId == -1 ? getFallbackItemId() : newId;
				id.setValue((short) newId);
			} else if (Tags.isCompoundTag(child) && name.equals("tag")) {
				for (Tag<?> grandChildren : child.getChildren()) {
					boolean canPrefix = grandChildren.toString().toLowerCase().startsWith("can");
					if (canPrefix && Tags.isListTag(grandChildren)) {
						for (Tag<?> t : grandChildren.getChildren()) {
							if (Tags.isShortTag(t)) {
								ShortTag id = (ShortTag) t;
								int oldId = id.getValue();
								int newId = getMapping(oldId);
								newId = newId == -1 ? getFallbackItemId() : newId;
								id.setValue((short) newId);
							}
						}
					}
				}
			}
		}
	}

}
